/*-------------------------------------------------------------------------
 *
 * enum.c
 *      I/O functions, operators, aggregates etc for enum types
 *
 * Copyright (c) 2006-2017, PostgreSQL Global Development Group
 *
 * This source code file contains modifications made by THL A29 Limited ("Tencent Modifications").
 * All Tencent Modifications are Copyright (C) 2023 THL A29 Limited.
 * 
 * IDENTIFICATION
 *      src/backend/utils/adt/enum.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/genam.h"
#include "access/heapam.h"
#include "access/htup_details.h"
#include "catalog/indexing.h"
#include "catalog/pg_enum.h"
#include "libpq/pqformat.h"
#include "storage/procarray.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/typcache.h"


static Oid    enum_endpoint(Oid enumtypoid, ScanDirection direction);
static ArrayType *enum_range_internal(Oid enumtypoid, Oid lower, Oid upper);


/*
 * Disallow use of an uncommitted pg_enum tuple.
 *
 * We need to make sure that uncommitted enum values don't get into indexes.
 * If they did, and if we then rolled back the pg_enum addition, we'd have
 * broken the index because value comparisons will not work reliably without
 * an underlying pg_enum entry.  (Note that removal of the heap entry
 * containing an enum value is not sufficient to ensure that it doesn't appear
 * in upper levels of indexes.)  To do this we prevent an uncommitted row from
 * being used for any SQL-level purpose.  This is stronger than necessary,
 * since the value might not be getting inserted into a table or there might
 * be no index on its column, but it's easy to enforce centrally.
 *
 * However, it's okay to allow use of uncommitted values belonging to enum
 * types that were themselves created in the same transaction, because then
 * any such index would also be new and would go away altogether on rollback.
 * (This case is required by pg_upgrade.)
 *
 * This function needs to be called (directly or indirectly) in any of the
 * functions below that could return an enum value to SQL operations.
 */
static void
check_safe_enum_use(HeapTuple enumval_tup)
{
    TransactionId xmin;
    Form_pg_enum en;
    HeapTuple    enumtyp_tup;

    /*
     * If the row is hinted as committed, it's surely safe.  This provides a
     * fast path for all normal use-cases.
     */
    if (HeapTupleHeaderXminCommitted(enumval_tup->t_data))
        return;

    /*
     * Usually, a row would get hinted as committed when it's read or loaded
     * into syscache; but just in case not, let's check the xmin directly.
     */
    xmin = HeapTupleHeaderGetXmin(enumval_tup->t_data);
    if (!TransactionIdIsInProgress(xmin) &&
        TransactionIdDidCommit(xmin))
        return;

    /* It is a new enum value, so check to see if the whole enum is new */
    en = (Form_pg_enum) GETSTRUCT(enumval_tup);
    enumtyp_tup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(en->enumtypid));
    if (!HeapTupleIsValid(enumtyp_tup))
        elog(ERROR, "cache lookup failed for type %u", en->enumtypid);

    /*
     * We insist that the type have been created in the same (sub)transaction
     * as the enum value.  It would be safe to allow the type's originating
     * xact to be a subcommitted child of the enum value's xact, but not vice
     * versa (since we might now be in a subxact of the type's originating
     * xact, which could roll back along with the enum value's subxact).  The
     * former case seems a sufficiently weird usage pattern as to not be worth
     * spending code for, so we're left with a simple equality check.
     *
     * We also insist that the type's pg_type row not be HEAP_UPDATED.  If it
     * is, we can't tell whether the row was created or only modified in the
     * apparent originating xact, so it might be older than that xact.  (We do
     * not worry whether the enum value is HEAP_UPDATED; if it is, we might
     * think it's too new and throw an unnecessary error, but we won't allow
     * an unsafe case.)
     */
    if (xmin == HeapTupleHeaderGetXmin(enumtyp_tup->t_data) &&
        !(enumtyp_tup->t_data->t_infomask & HEAP_UPDATED))
    {
        /* same (sub)transaction, so safe */
        ReleaseSysCache(enumtyp_tup);
        return;
    }

    /*
     * There might well be other tests we could do here to narrow down the
     * unsafe conditions, but for now just raise an exception.
     */
    ereport(ERROR,
            (errcode(ERRCODE_UNSAFE_NEW_ENUM_VALUE_USAGE),
             errmsg("unsafe use of new value \"%s\" of enum type %s",
                    NameStr(en->enumlabel),
                    format_type_be(en->enumtypid)),
             errhint("New enum values must be committed before they can be used.")));
}


/* Basic I/O support */

Datum
enum_in(PG_FUNCTION_ARGS)
{
    char       *name = PG_GETARG_CSTRING(0);
    Oid            enumtypoid = PG_GETARG_OID(1);
    Oid            enumoid;
    HeapTuple    tup;

    /* must check length to prevent Assert failure within SearchSysCache */
    if (strlen(name) >= NAMEDATALEN)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input value for enum %s: \"%s\"",
                        format_type_be(enumtypoid),
                        name)));

    tup = SearchSysCache2(ENUMTYPOIDNAME,
                          ObjectIdGetDatum(enumtypoid),
                          CStringGetDatum(name));
    if (!HeapTupleIsValid(tup))
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input value for enum %s: \"%s\"",
                        format_type_be(enumtypoid),
                        name)));

    /* check it's safe to use in SQL */
    check_safe_enum_use(tup);

    /*
     * This comes from pg_enum.oid and stores system oids in user tables. This
     * oid must be preserved by binary upgrades.
     */
    enumoid = HeapTupleGetOid(tup);

    ReleaseSysCache(tup);

    PG_RETURN_OID(enumoid);
}

Datum
enum_out(PG_FUNCTION_ARGS)
{
    Oid            enumval = PG_GETARG_OID(0);
    char       *result;
    HeapTuple    tup;
    Form_pg_enum en;

    tup = SearchSysCache1(ENUMOID, ObjectIdGetDatum(enumval));
    if (!HeapTupleIsValid(tup))
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
                 errmsg("invalid internal value for enum: %u",
                        enumval)));
    en = (Form_pg_enum) GETSTRUCT(tup);

    result = pstrdup(NameStr(en->enumlabel));

    ReleaseSysCache(tup);

    PG_RETURN_CSTRING(result);
}

/* Binary I/O support */
Datum
enum_recv(PG_FUNCTION_ARGS)
{
    StringInfo    buf = (StringInfo) PG_GETARG_POINTER(0);
    Oid            enumtypoid = PG_GETARG_OID(1);
    Oid            enumoid;
    HeapTuple    tup;
    char       *name;
    int            nbytes;

    name = pq_getmsgtext(buf, buf->len - buf->cursor, &nbytes);

    /* must check length to prevent Assert failure within SearchSysCache */
    if (strlen(name) >= NAMEDATALEN)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input value for enum %s: \"%s\"",
                        format_type_be(enumtypoid),
                        name)));

    tup = SearchSysCache2(ENUMTYPOIDNAME,
                          ObjectIdGetDatum(enumtypoid),
                          CStringGetDatum(name));
    if (!HeapTupleIsValid(tup))
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input value for enum %s: \"%s\"",
                        format_type_be(enumtypoid),
                        name)));

    /* check it's safe to use in SQL */
    check_safe_enum_use(tup);

    enumoid = HeapTupleGetOid(tup);

    ReleaseSysCache(tup);

    pfree(name);

    PG_RETURN_OID(enumoid);
}

Datum
enum_send(PG_FUNCTION_ARGS)
{
    Oid            enumval = PG_GETARG_OID(0);
    StringInfoData buf;
    HeapTuple    tup;
    Form_pg_enum en;

    tup = SearchSysCache1(ENUMOID, ObjectIdGetDatum(enumval));
    if (!HeapTupleIsValid(tup))
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
                 errmsg("invalid internal value for enum: %u",
                        enumval)));
    en = (Form_pg_enum) GETSTRUCT(tup);

    pq_begintypsend(&buf);
    pq_sendtext(&buf, NameStr(en->enumlabel), strlen(NameStr(en->enumlabel)));

    ReleaseSysCache(tup);

    PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
}

/* Comparison functions and related */

/*
 * enum_cmp_internal is the common engine for all the visible comparison
 * functions, except for enum_eq and enum_ne which can just check for OID
 * equality directly.
 */
static int
enum_cmp_internal(Oid arg1, Oid arg2, FunctionCallInfo fcinfo)
{
    TypeCacheEntry *tcache;

    /*
     * We don't need the typcache except in the hopefully-uncommon case that
     * one or both Oids are odd.  This means that cursory testing of code that
     * fails to pass flinfo to an enum comparison function might not disclose
     * the oversight.  To make such errors more obvious, Assert that we have a
     * place to cache even when we take a fast-path exit.
     */
    Assert(fcinfo->flinfo != NULL);

    /* Equal OIDs are equal no matter what */
    if (arg1 == arg2)
        return 0;

    /* Fast path: even-numbered Oids are known to compare correctly */
    if ((arg1 & 1) == 0 && (arg2 & 1) == 0)
    {
        if (arg1 < arg2)
            return -1;
        else
            return 1;
    }

    /* Locate the typcache entry for the enum type */
    tcache = (TypeCacheEntry *) fcinfo->flinfo->fn_extra;
    if (tcache == NULL)
    {
        HeapTuple    enum_tup;
        Form_pg_enum en;
        Oid            typeoid;

        /* Get the OID of the enum type containing arg1 */
        enum_tup = SearchSysCache1(ENUMOID, ObjectIdGetDatum(arg1));
        if (!HeapTupleIsValid(enum_tup))
            ereport(ERROR,
                    (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
                     errmsg("invalid internal value for enum: %u",
                            arg1)));
        en = (Form_pg_enum) GETSTRUCT(enum_tup);
        typeoid = en->enumtypid;
        ReleaseSysCache(enum_tup);
        /* Now locate and remember the typcache entry */
        tcache = lookup_type_cache(typeoid, 0);
        fcinfo->flinfo->fn_extra = (void *) tcache;
    }

    /* The remaining comparison logic is in typcache.c */
    return compare_values_of_enum(tcache, arg1, arg2);
}

Datum
enum_lt(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) < 0);
}

Datum
enum_le(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) <= 0);
}

Datum
enum_eq(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_BOOL(a == b);
}

Datum
enum_ne(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_BOOL(a != b);
}

Datum
enum_ge(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) >= 0);
}

Datum
enum_gt(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) > 0);
}

Datum
enum_smaller(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_OID(enum_cmp_internal(a, b, fcinfo) < 0 ? a : b);
}

Datum
enum_larger(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_OID(enum_cmp_internal(a, b, fcinfo) > 0 ? a : b);
}

Datum
enum_cmp(PG_FUNCTION_ARGS)
{
    Oid            a = PG_GETARG_OID(0);
    Oid            b = PG_GETARG_OID(1);

    PG_RETURN_INT32(enum_cmp_internal(a, b, fcinfo));
}

/* Enum programming support functions */

/*
 * enum_endpoint: common code for enum_first/enum_last
 */
static Oid
enum_endpoint(Oid enumtypoid, ScanDirection direction)
{
    Relation    enum_rel;
    Relation    enum_idx;
    SysScanDesc enum_scan;
    HeapTuple    enum_tuple;
    ScanKeyData skey;
    Oid            minmax;

    /*
     * Find the first/last enum member using pg_enum_typid_sortorder_index.
     * Note we must not use the syscache.  See comments for RenumberEnumType
     * in catalog/pg_enum.c for more info.
     */
    ScanKeyInit(&skey,
                Anum_pg_enum_enumtypid,
                BTEqualStrategyNumber, F_OIDEQ,
                ObjectIdGetDatum(enumtypoid));

    enum_rel = heap_open(EnumRelationId, AccessShareLock);
    enum_idx = index_open(EnumTypIdSortOrderIndexId, AccessShareLock);
    enum_scan = systable_beginscan_ordered(enum_rel, enum_idx, NULL,
                                           1, &skey);

    enum_tuple = systable_getnext_ordered(enum_scan, direction);
    if (HeapTupleIsValid(enum_tuple))
    {
        /* check it's safe to use in SQL */
        check_safe_enum_use(enum_tuple);
        minmax = HeapTupleGetOid(enum_tuple);
    }
    else
    {
        /* should only happen with an empty enum */
        minmax = InvalidOid;
    }

    systable_endscan_ordered(enum_scan);
    index_close(enum_idx, AccessShareLock);
    heap_close(enum_rel, AccessShareLock);

    return minmax;
}

Datum
enum_first(PG_FUNCTION_ARGS)
{
    Oid            enumtypoid;
    Oid            min;

    /*
     * We rely on being able to get the specific enum type from the calling
     * expression tree.  Notice that the actual value of the argument isn't
     * examined at all; in particular it might be NULL.
     */
    enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
    if (enumtypoid == InvalidOid)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("could not determine actual enum type")));

    /* Get the OID using the index */
    min = enum_endpoint(enumtypoid, ForwardScanDirection);

    if (!OidIsValid(min))
        ereport(ERROR,
                (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
                 errmsg("enum %s contains no values",
                        format_type_be(enumtypoid))));

    PG_RETURN_OID(min);
}

Datum
enum_last(PG_FUNCTION_ARGS)
{
    Oid            enumtypoid;
    Oid            max;

    /*
     * We rely on being able to get the specific enum type from the calling
     * expression tree.  Notice that the actual value of the argument isn't
     * examined at all; in particular it might be NULL.
     */
    enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
    if (enumtypoid == InvalidOid)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("could not determine actual enum type")));

    /* Get the OID using the index */
    max = enum_endpoint(enumtypoid, BackwardScanDirection);

    if (!OidIsValid(max))
        ereport(ERROR,
                (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
                 errmsg("enum %s contains no values",
                        format_type_be(enumtypoid))));

    PG_RETURN_OID(max);
}

/* 2-argument variant of enum_range */
Datum
enum_range_bounds(PG_FUNCTION_ARGS)
{
    Oid            lower;
    Oid            upper;
    Oid            enumtypoid;

    if (PG_ARGISNULL(0))
        lower = InvalidOid;
    else
        lower = PG_GETARG_OID(0);
    if (PG_ARGISNULL(1))
        upper = InvalidOid;
    else
        upper = PG_GETARG_OID(1);

    /*
     * We rely on being able to get the specific enum type from the calling
     * expression tree.  The generic type mechanism should have ensured that
     * both are of the same type.
     */
    enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
    if (enumtypoid == InvalidOid)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("could not determine actual enum type")));

    PG_RETURN_ARRAYTYPE_P(enum_range_internal(enumtypoid, lower, upper));
}

/* 1-argument variant of enum_range */
Datum
enum_range_all(PG_FUNCTION_ARGS)
{
    Oid            enumtypoid;

    /*
     * We rely on being able to get the specific enum type from the calling
     * expression tree.  Notice that the actual value of the argument isn't
     * examined at all; in particular it might be NULL.
     */
    enumtypoid = get_fn_expr_argtype(fcinfo->flinfo, 0);
    if (enumtypoid == InvalidOid)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("could not determine actual enum type")));

    PG_RETURN_ARRAYTYPE_P(enum_range_internal(enumtypoid,
                                              InvalidOid, InvalidOid));
}

static ArrayType *
enum_range_internal(Oid enumtypoid, Oid lower, Oid upper)
{// #lizard forgives
    ArrayType  *result;
    Relation    enum_rel;
    Relation    enum_idx;
    SysScanDesc enum_scan;
    HeapTuple    enum_tuple;
    ScanKeyData skey;
    Datum       *elems;
    int            max,
                cnt;
    bool        left_found;

    /*
     * Scan the enum members in order using pg_enum_typid_sortorder_index.
     * Note we must not use the syscache.  See comments for RenumberEnumType
     * in catalog/pg_enum.c for more info.
     */
    ScanKeyInit(&skey,
                Anum_pg_enum_enumtypid,
                BTEqualStrategyNumber, F_OIDEQ,
                ObjectIdGetDatum(enumtypoid));

    enum_rel = heap_open(EnumRelationId, AccessShareLock);
    enum_idx = index_open(EnumTypIdSortOrderIndexId, AccessShareLock);
    enum_scan = systable_beginscan_ordered(enum_rel, enum_idx, NULL, 1, &skey);

    max = 64;
    elems = (Datum *) palloc(max * sizeof(Datum));
    cnt = 0;
    left_found = !OidIsValid(lower);

    while (HeapTupleIsValid(enum_tuple = systable_getnext_ordered(enum_scan, ForwardScanDirection)))
    {
        Oid            enum_oid = HeapTupleGetOid(enum_tuple);

        if (!left_found && lower == enum_oid)
            left_found = true;

        if (left_found)
        {
            /* check it's safe to use in SQL */
            check_safe_enum_use(enum_tuple);

            if (cnt >= max)
            {
                max *= 2;
                elems = (Datum *) repalloc(elems, max * sizeof(Datum));
            }

            elems[cnt++] = ObjectIdGetDatum(enum_oid);
        }

        if (OidIsValid(upper) && upper == enum_oid)
            break;
    }

    systable_endscan_ordered(enum_scan);
    index_close(enum_idx, AccessShareLock);
    heap_close(enum_rel, AccessShareLock);

    /* and build the result array */
    /* note this hardwires some details about the representation of Oid */
    result = construct_array(elems, cnt, enumtypoid, sizeof(Oid), true, 'i');

    pfree(elems);

    return result;
}
